iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 8
0
Modern Web

Angular新手村學習筆記(2019)系列 第 8

Day08_Form Part I - Template-Driven Form

  • 分享至 

  • xImage
  •  

[S06E07] Form - Template-Driven Form

https://www.youtube.com/watch?v=t7MEvi9RdNI&list=PL9LUW6O9WZqgUMHwDsKQf3prtqVvjGZ6S&index=6

Template-Driven Form 內容簡單,適合新手入門

適用情境

  • 表單很簡單,沒有複雜的驗證規則,例如:連動的欄位驗證
  • 不需要自定義validator(自定義validator很麻煩)
  • 不需要動態表單

Template-Driven Form VS Reactive Form

  • Reactive Form需要搭配RxJS,可讓HTML少寫很多東西
  • Reactive Form熟練後會回不去Template-Driven Form
  • Template-Driven Form屬非同步行為,在取得表單實體是有限制的,需注意生命週期
    測試時需留意:每次畫面更新,需做一次CD(change detection)才會更新到model
    @Input xxx:xxxType
    ngOnChanges(changes: SimpleChanges) { ... }
    
  • Reactive Form的表單實體一開始就存在

本集開始沒看文件,Kevin 老師直接寫程式

首先,先開好片頭導讀的文章,再跟著影片一起看,以後才有能力自己查文件

  1. 開啟 https://angular.io
  2. FUNDAMENTALS / Forms / Template-driven Forms
    https://angular.io/guide/forms#template-driven-forms

今天內容有:(方便search)

  • NgForm (DIRECTIVE)
  • FormControl
  • 透過template reference直接取某個control
  • 文件導讀 FUNDAMENTALS / Forms / Template-driven Forms
  • <select>選單
  • select option裡的value若要綁Object
  • Angular selects option 的 <select> CompareWith input
  • input element的雙向綁定
  • 樣式的表達
  • 送出表單 submit
  • input加disable時,如何取得所有的內容?
  • template-driven 的 自定義表單驗證(template-driven-validation)

在app.module.ts要imports:[FormsModule]

  1. app.component.html

但Template-Driven Form有其起手式,我們來看最簡單的形式

<form #f="ngForm"> 為了取值,透過templateRef吐出ngForm(最上層的FormGroup)
      ^^^^^^^^^^ 非必加  
    <input name="firstName" ngModel /> name跟ngModel必加
           ^^^^             ^^^^^^^ 必加,否則立刻報錯
</form>

{{ f.value | json }} 會輸出整個form的東西
請參考:NgForm (DIRECTIVE)的value屬性

關鍵文件導讀

NgForm、NgModel、FormControl 之間的關係,跟各自有哪些屬性、方法可用?
就我的理解
NgForm是最上層的FormGroup instance
NgModel 會從 domain model 去建立一個 FormControl

NgForm (DIRECTIVE)

API > @angular/forms
https://angular.io/api/forms/NgForm
建一個最上層的FormGroup instance,並"綁定"form後就能:

  • 能抓到所有form control
  • 使用form value
  • validation status(驗證狀態)

FormGroup // 繼承 forms/AbstractControl
https://angular.io/api/forms/FormGroup

NgModel

API > @angular/forms

  1. 開啟 https://angular.io,右上Search找NgModel DIRECTIVE(紅色)
    https://angular.io/api/forms/NgModel
    NgModel 會從 domain model 去建立一個 FormControl 並bind(綁定) form control element

FormControl

https://angular.io/api/forms/FormControl
Tracks the value and validation status of an individual form control.
無論在Reactive Form或Template-Driven Form,都用FormControl
form裡面的input element就代表一個FormControl

<form #f="ngForm">
    <input name="firstName" ngModel /> input element這就是FormControl
</form>

FormControl包含什麼?以input element為例,包含

class FormControl extends AbstractControl {
  constructor(formState: any = null, validatorOrOpts?: ValidatorFn | AbstractControlOptions | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[])
  ...

  // inherited from forms/AbstractControl 繼承forms/AbstractControl
  constructor(validator: ValidatorFn, asyncValidator: AsyncValidatorFn)
  // Template-Driven Form 主要是對 屬性 的熟悉度,例如:
  // 該contorl目前的值,有4類(簡單的值、key-value pair、object、array),可先console.log出來
  value: any 
  
  // 指定一個 ValidatorFn 來驗證control的值是否合法
  validator: ValidatorFn | null
  
  // 狀態
  status: string // VALID | INVALID | PENDING(該control正在檢查) | DISABLED(此control不受檢查)
  
  // 值有沒有被改過
  dirty: boolean
  
  // method(方法),在Template-Driven Form,能用的method較少,主要是 屬性、狀態 用較多
  // 設定表單驗證
  setErrors(errors: ValidationErrors, opts: { emitEvent?: boolean; } = {}): void

透過template reference直接取某個control

<form #f="ngForm">
    <input name="firstName" ngModel #n="ngModel"/> input element這就是FormControl
</form>
{{ n.value | json }}
{{ n.valid }}
請參考FormControl有哪些 屬性 可用
https://angular.io/api/forms/FormControl

文件導讀 FUNDAMENTALS / Forms / Template-driven Forms

Create an initial HTML form template
https://angular.io/guide/forms#create-an-initial-html-form-template
在Template Driven Forms中,只要import FormsModule,
即使是HTML5的一般的form表單,也可適用FormsModule

import { FormsModule }   from '@angular/forms';
@NgModule({
  imports: [
    FormsModule // import FormsModule
  ],
}) 
export class AppModule { }

不用對<form>執行任何操作即可使用FormsModule

範例中的CSS跟FormsModule無關,class="form-group"單純只是bootstrap的樣式

@import url('https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css');
    <form>
      class裡的form-group與angular的FormGroup無關喔!!
      <div class="form-group"> // class不是必要的,只是bootstrap的表單樣式
        <label for="name">Name</label>
        <input type="text" class="form-control" id="name" required>
      </div>
      <button type="submit" class="btn btn-success">Submit</button>
    </form>

<select>選單

遇到選單用*ngFor

<div class="form-group">
  <label for="power">Hero Power</label>
  <select class="form-control" id="power" required>
    <option *ngFor="let pow of powers" [value]="pow">{{pow}}</option>
                                       control的value可以直接帶進去
  </select>
</div>

select option裡的value若要綁Object

請參考:NgSelectOption (Directive) 裡的 @Input()ngValue:any
https://angular.io/api/forms/NgSelectOption
NgSelectOption 的 屬性:

  • @Input() ngValue: any
    如果是Object,就用ngValue綁定
  • @Input() value: any
    如果是string value,就用value綁定

Angular selects option 的 <select> CompareWith input

在NgSelectOption DIRECTIVE按右邊的<>
https://angular.io/api/forms/NgSelectOption
就會進到程式碼
https://github.com/angular/angular/blob/8.2.5/packages/forms/src/directives/select_control_value_accessor.ts#L191-L256
再搜尋compareWith

const selectedCountriesControl = new FormControl();

當selectOption使用ngValue時,通常在<select>[compareWith]指定一個function()
當傳入的值 ngValue 是 Object 的時候,若要設定預設值,要加 [compareWith]="compareFn"

<select [compareWith]="compareFn"  [formControl]="selectedCountriesControl">
    <option *ngFor="let country of countries" [ngValue]="country">
        {{country.name}}
    </option>
</select>

compareFn(c1: Country, c2: Country): boolean {
    // 用id來比對
     return c1 && c2 ? c1.id === c2.id : c1 === c2;
}

input element的雙向綁定

<input type="text" class="form-control" id="name"
      required
      [(ngModel)]="model.name" name="name">
      ^^^^^^^^^^^^^^^^^^^^^^^
TODO: remove this: {{model.name}}

樣式的表達

共有3種狀態:

  • 該control被touched? 是的話,會多出 .ng-touched 不是的話 .ng-untouched
  • 該control的值有改變? 是的話,會多出 .ng-dirty 不是的話 .ng-pristine
  • 該control的value是不是valid? 是的話,會多出 ng-valid 不是的話 ng-invalid
    觀察Elements大概長這樣:
<form ...>
    <input _ngcontent-c68 name="firstName" ngmodel required ng-reflect-required ng-reflect-name="firstName" ng-reflect-model
    class="ng-pristine ng-invalid ng-touched">
            ^^^^^^^^^   ^^^^^^^^   ^^^^^^^^ 依input狀態,Angular自動幫你替換
</form>

範例:

  1. src/assets/forms.css 可以設計樣式
.ng-valid[required], .ng-valid.required  {
  border-left: 5px solid #42A948; /* green */
}

.ng-invalid:not(form)  {
  border-left: 5px solid #a94442; /* red */
}
  1. src/index.html
加在index.html 適用全專案的forms
<link rel="stylesheet" href="assets/forms.css">
  1. some.component.html
    效果可參考官方文件
<input type="text" class="form-control" id="name"
  required
  [(ngModel)]="model.name" name="name"
  #spy>
<br>TODO: remove this: {{spy.className}}
<form #f="ngForm">
      ^^ ngForm export出來的樣板變數
    <label for="name">Name</label>
    <input type="text" class="form-control" id="name"
           required minlength="3"
           ^^^^^^^  ^^^^^^^^^  直接在input加上驗證條件即可
           Template-Driven的自定義validator應該較麻煩,老師沒演示
           [(ngModel)]="model.name" name="name"
           #name="ngModel">
           ^^^^^template reference
</form>
{{ f.value | json }} 觀察form的value

<div [hidden]="name.valid || name.pristine"
               ^^^^利用template reference取到
     class="alert alert-danger">
  Name is required
</div>

用hasError('error的類型') 可知發生什麼錯誤 => 可自定義錯誤訊息
<span *ngIf="name.hasError('required')"> 當有name有required的error時
欄位必填                     ^^^^^^^^^ 此input control有無required錯誤訊息?
(可自定義錯誤訊息)
</span>

{{ name.valid }} 當control驗證失敗時  =>  false
{{ name.errors | json }} 當control的errors就會包含 => {"required":true }

送出表單 submit

  1. app.component.html
<form #f="ngForm" (ngSubmit)="save(f)">
                    ^^^^^^ 方法二:直接用(ngSubmit)
                    ^^^^^^ 只能在Template Driven使用,無法用於Reactive Form
    <input name="firstName" ngModel #n="ngModel" />
    方法一:<button type="submit"        (click)="save(f)">
                   ^^^當type為submit時    ^^ 把form group的內容傳到ts裡
</form>
  1. app.component.ts
save(f){
    console.log(f); // 觀察
    // controls      有哪些control
    // NgForm.form   這才是FormsGroup
    //             .getRawValue 取得所有value,包含disabled的input element
    // NgForm.EventEmitter
    // NgForm.value:Object
    //        firstName: "123"
    // 
}

input加disable時,如何取得所有的內容?

<form #f="ngForm">
    <input name="firstName" ngModel #n1="ngModel" />
                                    ^^ template reference不能重複
    <input name="lastName" ngModel #n2="ngModel" disabled/>
                                   ^^            ^^^^^^^^ 若disabled印不出value
</form>
{{ f.value | json }} 只會顯示 { "firstName":"" }
{{ f.form.getRawValue() | json } 用form.getRawValue()取得所有value

template-driven 的 自定義表單驗證(template-driven-validation)

  1. 開啟 https://angular.io
  2. FUNDAMENTALS / Forms / Form Validation # template-driven-validation
    https://angular.io/guide/form-validation#template-driven-validation
@Directive({ // 其實是Directive
  selector: '[appForbiddenName]',
  providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
                        ^^^^^^必須註冊在NG_VALIDATORS         multi:true一定要加   ^^^^
})
export class ForbiddenValidatorDirective implements Validator {
  @Input('appForbiddenName') forbiddenName: string;
  
  validate(control: AbstractControl): {[key: string]: any} | null {
  ^^^^^^^^ 實作驗證用的function()       ^^^^^^^^^^^^^^^^^^^^^^^^^^回傳Object或null
    return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control) : null;
  }
}

上一篇
Day07_Router
下一篇
Day09_Form Part II - Reactive Form
系列文
Angular新手村學習筆記(2019)33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言